﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Drawing;
using System.Drawing.Imaging;
using System.Diagnostics;

using DirectShowLib;


namespace Webcam.VideoSource
{
    /// <summary>
    /// Represents a webcam
    /// </summary>
    public class Webcam : ISampleGrabberCB, IDisposable
    {
        #region Member variables

        /// <summary> graph builder interface. </summary>
        private IFilterGraph2 m_FilterGraph = null;
        private IMediaControl m_mediaCtrl = null;

        /// <summary> Indicates the status of the graph </summary>
        private bool m_bRunning = false;

        /// <summary> Dimensions of the image, calculated once in constructor. </summary>
        private IntPtr m_handle = IntPtr.Zero;
        private int m_videoWidth;
        private int m_videoHeight;
        private int m_stride;
        public int m_Dropped = 0;

        #endregion


        /// <summary> Use capture device zero, default frame rate and size</summary>
        public Webcam()
        {
            _Webcam(0, 0, 0, 0);
        }
        /// <summary> Use specified capture device, default frame rate and size</summary>
        public Webcam(int iDeviceNum)
        {
            _Webcam(iDeviceNum, 0, 0, 0);
        }
        /// <summary> Use specified capture device, specified frame rate and default size</summary>
        public Webcam(int iDeviceNum, int iFrameRate)
        {
            _Webcam(iDeviceNum, iFrameRate, 0, 0);
        }
        /// <summary> Use specified capture device, specified frame rate and size</summary>
        public Webcam(int iDeviceNum, int iFrameRate, int iWidth, int iHeight)
        {
            _Webcam(iDeviceNum, iFrameRate, iWidth, iHeight);
        }
        /// <summary> release everything. </summary>
        public void Dispose()
        {
            CloseInterfaces();
        }
        // Destructor
        ~Webcam()
        {
            Dispose();
        }

        public int Width
        {
            get
            {
                return m_videoWidth;
            }
        }
        public int Height
        {
            get
            {
                return m_videoHeight;
            }
        }
        public int Stride
        {
            get
            {
                return m_stride;
            }
        }

        // Start the capture graph
        public void Start()
        {
            if (!m_bRunning)
            {
                int hr = m_mediaCtrl.Run();
                DsError.ThrowExceptionForHR(hr);

                m_bRunning = true;
            }
        }
        // Pause the capture graph.
        // Running the graph takes up a lot of resources.  Pause it when it
        // isn't needed.
        public void Pause()
        {
            if (m_bRunning)
            {
                int hr = m_mediaCtrl.Pause();
                DsError.ThrowExceptionForHR(hr);

                m_bRunning = false;
            }
        }

        /// <summary>
        /// The device number in the system
        /// </summary>
        public int DeviceNumber = -1;

        // Internal capture
        private void _Webcam(int iDeviceNum, int iFrameRate, int iWidth, int iHeight)
        {
            this.DeviceNumber = iDeviceNum;
            DsDevice[] capDevices;

            // Get the collection of video devices
            capDevices = DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice);

            if (iDeviceNum + 1 > capDevices.Length)
            {
                throw new Exception("No video capture devices found at that index!");
            }

            try
            {
                // Set up the capture graph
                SetupGraph(capDevices[iDeviceNum], iFrameRate, iWidth, iHeight);

                // tell the callback to ignore new images
                m_bRunning = false;
            }
            catch
            {
                Dispose();
                throw;
            }
        }

        /// <summary> build the capture graph for grabber. </summary>
        private void SetupGraph(DsDevice dev, int iFrameRate, int iWidth, int iHeight)
        {
            int hr;

            ISampleGrabber sampGrabber = null;
            IBaseFilter capFilter = null;
            ICaptureGraphBuilder2 capGraph = null;

            // Get the graphbuilder object
            m_FilterGraph = (IFilterGraph2)new FilterGraph();
            m_mediaCtrl = m_FilterGraph as IMediaControl;
            try
            {
                // Get the ICaptureGraphBuilder2
                capGraph = (ICaptureGraphBuilder2)new CaptureGraphBuilder2();

                // Get the SampleGrabber interface
                sampGrabber = (ISampleGrabber)new SampleGrabber();

                // Start building the graph
                hr = capGraph.SetFiltergraph(m_FilterGraph);
                DsError.ThrowExceptionForHR(hr);

                // Add the video device
                hr = m_FilterGraph.AddSourceFilterForMoniker(dev.Mon, null, "Video input", out capFilter);
                DsError.ThrowExceptionForHR(hr);

                IBaseFilter baseGrabFlt = (IBaseFilter)sampGrabber;
                ConfigureSampleGrabber(sampGrabber);

                // Add the frame grabber to the graph
                hr = m_FilterGraph.AddFilter(baseGrabFlt, "Ds.NET Grabber");
                DsError.ThrowExceptionForHR(hr);

                // If any of the default config items are set
                if (iFrameRate + iHeight + iWidth > 0)
                {
                    SetConfigParms(capGraph, capFilter, iFrameRate, iWidth, iHeight);
                }

                hr = capGraph.RenderStream(PinCategory.Capture, MediaType.Video, capFilter, null, baseGrabFlt);
                DsError.ThrowExceptionForHR(hr);

                SaveSizeInfo(sampGrabber);
            }
            finally
            {
                if (capFilter != null)
                {
                    Marshal.ReleaseComObject(capFilter);
                    capFilter = null;
                }
                if (sampGrabber != null)
                {
                    Marshal.ReleaseComObject(sampGrabber);
                    sampGrabber = null;
                }
                if (capGraph != null)
                {
                    Marshal.ReleaseComObject(capGraph);
                    capGraph = null;
                }
            }
        }

        private void SaveSizeInfo(ISampleGrabber sampGrabber)
        {
            int hr;

            // Get the media type from the SampleGrabber
            AMMediaType media = new AMMediaType();
            hr = sampGrabber.GetConnectedMediaType(media);
            DsError.ThrowExceptionForHR(hr);

            if ((media.formatType != FormatType.VideoInfo) || (media.formatPtr == IntPtr.Zero))
            {
                throw new NotSupportedException("Unknown Grabber Media Format");
            }

            // Grab the size info
            VideoInfoHeader videoInfoHeader = (VideoInfoHeader)Marshal.PtrToStructure(media.formatPtr, typeof(VideoInfoHeader));
            m_videoWidth = videoInfoHeader.BmiHeader.Width;
            m_videoHeight = videoInfoHeader.BmiHeader.Height;
            m_stride = m_videoWidth * (videoInfoHeader.BmiHeader.BitCount / 8);

            DsUtils.FreeAMMediaType(media);
            media = null;
        }
        private void ConfigureSampleGrabber(ISampleGrabber sampGrabber)
        {
            AMMediaType media;
            int hr;

            // Set the media type to Video/RBG24
            media = new AMMediaType();
            media.majorType = MediaType.Video;
            media.subType = MediaSubType.RGB24;
            media.formatType = FormatType.VideoInfo;
            hr = sampGrabber.SetMediaType(media);
            DsError.ThrowExceptionForHR(hr);

            DsUtils.FreeAMMediaType(media);
            media = null;

            // Configure the samplegrabber
            hr = sampGrabber.SetCallback(this, 1);
            DsError.ThrowExceptionForHR(hr);
        }

        // Set the Framerate, and video size
        private void SetConfigParms(ICaptureGraphBuilder2 capGraph, IBaseFilter capFilter, int iFrameRate, int iWidth, int iHeight)
        {
            int hr;
            object o;
            AMMediaType media;

            // Find the stream config interface
            hr = capGraph.FindInterface(
                PinCategory.Capture, MediaType.Video, capFilter, typeof(IAMStreamConfig).GUID, out o);

            IAMStreamConfig videoStreamConfig = o as IAMStreamConfig;
            if (videoStreamConfig == null)
            {
                throw new Exception("Failed to get IAMStreamConfig");
            }

            // Get the existing format block
            hr = videoStreamConfig.GetFormat(out media);
            DsError.ThrowExceptionForHR(hr);

            // copy out the videoinfoheader
            VideoInfoHeader v = new VideoInfoHeader();
            Marshal.PtrToStructure(media.formatPtr, v);

            // if overriding the framerate, set the frame rate
            if (iFrameRate > 0)
            {
                v.AvgTimePerFrame = 10000000 / iFrameRate;
            }

            // if overriding the width, set the width
            if (iWidth > 0)
            {
                v.BmiHeader.Width = iWidth;
            }

            // if overriding the Height, set the Height
            if (iHeight > 0)
            {
                v.BmiHeader.Height = iHeight;
            }

            // Copy the media structure back
            Marshal.StructureToPtr(v, media.formatPtr, false);

            // Set the new format
            hr = videoStreamConfig.SetFormat(media);
            DsError.ThrowExceptionForHR(hr);

            DsUtils.FreeAMMediaType(media);
            media = null;
        }

        /// <summary> Shut down capture </summary>
        private void CloseInterfaces()
        {
            int hr;

            try
            {
                if (m_mediaCtrl != null)
                {
                    // Stop the graph
                    hr = m_mediaCtrl.Stop();
                    m_bRunning = false;
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }

            if (m_FilterGraph != null)
            {
                Marshal.ReleaseComObject(m_FilterGraph);
                m_FilterGraph = null;
            }
        }

        /// <summary> sample callback, NOT USED. </summary>
        int ISampleGrabberCB.SampleCB(double SampleTime, IMediaSample pSample)
        {
            //if (!m_bGotOne)
            //{
            //    // Set bGotOne to prevent further calls until we
            //    // request a new bitmap.
            //    m_bGotOne = true;
            //    IntPtr pBuffer;

            //    pSample.GetPointer(out pBuffer);
            //    int iBufferLen = pSample.GetSize();

            //    if (pSample.GetSize() > m_stride * m_videoHeight)
            //    {
            //        throw new Exception("Buffer is wrong size");
            //    }

            //    CopyMemory(m_handle, pBuffer, m_stride * m_videoHeight);

            //    // Picture is ready.
            //    m_PictureReady.Set();
            //}

            //Marshal.ReleaseComObject(pSample);
            return 0;
        }

        /// <summary>
        /// The time the previous frame was captured
        /// </summary>
        DateTime previous = DateTime.Now;

        /// <summary> buffer callback, COULD BE FROM FOREIGN THREAD. </summary>
        int ISampleGrabberCB.BufferCB(double SampleTime, IntPtr pBuffer, int BufferLen)
        {
            TimeSpan diff = DateTime.Now - previous;
            double fps = 1000.0 / diff.TotalMilliseconds;
            previous = DateTime.Now;


            // The buffer should be long enough
            if (BufferLen <= m_stride * m_videoHeight)
            {
                // Copy the frame to the buffer
                //CopyMemory(m_handle, pBuffer, m_stride * m_videoHeight);
                Bitmap b = MakeBitmap(pBuffer, Width, Height);
                AddFrame(new Frame(b, diff));
            }
            else
            {
                throw new Exception("Buffer is wrong size");
            }

            return 0;
        }


        unsafe static Bitmap MakeBitmap(IntPtr source, int Width, int Height)
        {
            try
            {
                Bitmap b = new Bitmap(Width, Height);
                BitmapData d = b.LockBits
                    (
                        new Rectangle(0, 0, Width, Height),
                        ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb
                        );

                // bytes per pixel
                int pixelSize = 3;

                for (int y = 0; y < Height; y++)
                {
                    //get the data from the original image
                    //byte* oRow = (byte*)source + (y * d.Stride);
                    byte* oRow = ((byte*)source + ((Height - y - 1) * d.Stride));

                    //get the data from the new image
                    byte* nRow = (byte*)d.Scan0 + (y * d.Stride);

                    for (int x = 0; x < Width; x++)
                    {
                        nRow[x * pixelSize] = oRow[x * pixelSize]; //B
                        nRow[x * pixelSize + 1] = oRow[x * pixelSize + 1]; //G
                        nRow[x * pixelSize + 2] = oRow[x * pixelSize + 2]; //R
                    }
                }

                b.UnlockBits(d);
                return b;
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
            return new Bitmap(Width, Height);
        }


        /// <summary>
        /// The images captured from the webcam
        /// </summary>
        Stack<Frame> Frames = new Stack<Frame>();

        /// <summary>
        /// Add a frame to the frames list
        /// </summary>
        /// <param name="f"></param>
        public void AddFrame(Frame f)
        {
            
            lock (Frames)
            {
                // if too many frames, remove some
                if (Frames.Count >= FramesToHold * 2)
                {
                    // move the frames that you want to keep into a temp collection
                    Queue<Frame> temp = new Queue<Frame>();
                    for (int i = 0; i < FramesToHold - 1; i++)
                    {
                        temp.Enqueue(Frames.Pop());
                    }

                    // clear any remaining frames
                    Frames.Clear();

                    // add the old frames back into the collection
                    for (int i = 0; i < temp.Count; i++)
                    {
                        Frames.Push(temp.Dequeue());
                    }
                    
                }
                Frames.Push(f);
            }
        }

        /// <summary>
        /// The number of frames to hold in the frames collection
        /// </summary>
        public int FramesToHold = 2;

        /// <summary>
        /// Get most recent frame from the webcam
        /// </summary>
        /// <returns></returns>
        public Frame GetFrame()
        {
            Frame f = null;
            lock (Frames)
            {
                if (Frames.Count > 0)
                    f = Frames.Pop();
            }
            return f;
        }

    }

}
